package org.sercho.masp.notification;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.jms.IllegalStateException;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.UnmarshalException;
import javax.xml.bind.Unmarshaller;

import org.sercho.masp.distribution.context.ContextConnection;
import org.sercho.masp.models.MASPDirectory;
import org.sercho.masp.models.Context.InteractionResource;
import org.sercho.masp.notification.decision.DecisionEngine;
import org.sercho.masp.notification.decision.DecisionResult;
import org.sercho.masp.notification.settings.NotificationSettings;
import org.sercho.masp.notification.settings.NotificationSettings.MessageCategory;
import org.sercho.masp.notification.settings.NotificationSettings.TimeOfNotification;

/**
 * This singleton is the controller-class for the notification-system. It
 * provides methods for registering message-categories and calculating devices
 * for presenting corresponding messages, as well as for adding user-specific
 * notification-urgencies. Further it provides methods to get and save the
 * {@link NotificationSettings}.
 * 
 * @author Benny
 * 
 */
public class NotificationSystem {

	/**
	 * The only existing instance of this singleton.
	 */
	private static NotificationSystem instance;
	/**
	 * The {@link NotificationSettings}-object.
	 */
	private NotificationSettings settings;
	/**
	 * The {@link JAXBContext} for loading and storing the
	 * {@link NotificationSettings}-object.
	 */
	private JAXBContext jaxb;
	/**
	 * The {@link Marshaller} for storing the {@link NotificationSettings}
	 * -object.
	 */
	private Marshaller marsh;
	/**
	 * The {@link Unmarshaller} for loading the {@link NotificationSettings}
	 * -object.
	 */
	private Unmarshaller unmarsh;
	/**
	 * The {@link DecisionEngine} for making all decisions concerning a
	 * notification.
	 */
	private DecisionEngine decEngine;

	/**
	 * Constructor.
	 */
	private NotificationSystem() {
		this.decEngine = DecisionEngine.getInstance();
	}

	/**
	 * This method returns the actual singleton-instance. If no instance exists
	 * yet, a new one will be created.
	 * 
	 * @return {@link NotificationSystem}
	 */
	public static NotificationSystem getInstance() {
		if (NotificationSystem.instance == null) {
			NotificationSystem.instance = new NotificationSystem();
		}
		return NotificationSystem.instance;
	}

	/**
	 * Loads, if necessary, and returns the {@link NotificationSettings}.
	 * 
	 * @return {@link NotificationSettings}
	 */
	public NotificationSettings getSettings() {
		if (this.settings == null) {
			String path = MASPDirectory.getMASPDirectoryPath();
			File settingsFile = new File(path, "NotificationSettings.xml");
			if (settingsFile.exists()) {
				try {
					this.settings = (NotificationSettings) this
							.getUnmarshaller().unmarshal(settingsFile);
				} catch (UnmarshalException unmarshEx) {
					this.settings = new NotificationSettings();
					this.saveSettings();
				} catch (JAXBException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			} else {
				this.settings = new NotificationSettings();
				this.saveSettings();
			}
		}
		return this.settings;
	}

	/**
	 * Stores the {@link NotificationSettings} in a XML-file.
	 */
	public void saveSettings() {
		String path = MASPDirectory.getMASPDirectoryPath();
		File settingsFile = new File(path, "NotificationSettings.xml");
		try {
			settingsFile.createNewFile();
			this.getMarshaller().marshal(this.getSettings(), settingsFile);
		} catch (JAXBException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	/**
	 * Creates, if necessary, and returns the {@link JAXBContext} for
	 * {@link NotificationSettings}.
	 * 
	 * @return {@link JAXBContext}
	 * @throws JAXBException
	 *             thrown by {@link JAXBContext#newInstance(Class...)}
	 */
	private JAXBContext getJAXBContext() throws JAXBException {
		if (this.jaxb == null) {
			this.jaxb = JAXBContext.newInstance(NotificationSettings.class);
		}
		return this.jaxb;
	}

	/**
	 * Creates, if necessary, and returns the {@link Marshaller} for
	 * {@link NotificationSettings}.
	 * 
	 * @return {@link Marshaller}
	 * @throws JAXBException
	 *             thrown by {@link JAXBContext#createMarshaller()}
	 */
	private Marshaller getMarshaller() throws JAXBException {
		if (this.marsh == null) {
			this.marsh = this.getJAXBContext().createMarshaller();
			this.marsh.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
		}
		return this.marsh;
	}

	/**
	 * Creates, if necessary, and returns the {@link Unmarshaller} for
	 * {@link NotificationSettings}.
	 * 
	 * @return {@link Unmarshaller}
	 * @throws JAXBException
	 *             thrown by {@link JAXBContext#createUnmarshaller()}
	 */
	private Unmarshaller getUnmarshaller() throws JAXBException {
		if (this.unmarsh == null) {
			this.unmarsh = this.getJAXBContext().createUnmarshaller();
		}
		return this.unmarsh;
	}

	/**
	 * A message-source can register one of it's message-categories along with
	 * possible times of notifications and corresponding urgencies. E.g. a
	 * source can register a message-category "doctor appointment". The source
	 * would send a message of this category at different times, which would be
	 * "1 day before appointment", "3 hours before appointment" and
	 * "30 minutes before appointment". These times will be registered, too,
	 * alongside default urgencies for each of these times. <br>
	 * <br>
	 * If this method is called again when a source already registered a
	 * message-category for the given ID, the old registration will be simply
	 * overwritten. <br>
	 * <br>
	 * The sourceId and the messageCategoryId are required.
	 * 
	 * @param sourceId
	 *            the ID of the message-source
	 * @param messageCategoryId
	 *            the ID for the message-category to be registered
	 * @param category
	 *            a name for the message-category, e.g. to list all categories
	 *            to the user
	 * @param timesOfNotification
	 *            a map of IDs for the different times of notification to a
	 *            short description of these times. The ID is used to map a time
	 *            to an urgency and the description to eventually show it to the
	 *            user.
	 * @param urgencies
	 *            a map of IDs of the registered times of notification to a
	 *            urgency from 1 to 5 for the corresponding time of
	 *            notification. Any urgencies higher than that range will be
	 *            modified to 5, any lower ones than to 1.
	 * @throws IllegalStateException
	 *             if sourceId or messageCategoryId are null or empty
	 */
	public void registerMessage(String sourceId, String messageCategoryId,
			String category, Map<String, String> timesOfNotification,
			Map<String, Integer> urgencies) throws IllegalStateException {
		if (sourceId == null || sourceId.isEmpty() || messageCategoryId == null
				|| messageCategoryId.isEmpty()) {
			throw new IllegalStateException(
					"sourceId and messageCategoryId are required!");
		}
		List<TimeOfNotification> timesList = new ArrayList<TimeOfNotification>();
		Set<String> keys = timesOfNotification.keySet();
		for (String key : keys) {
			Integer urgency = urgencies.get(key);
			if (urgency != null) {
				TimeOfNotification time = new TimeOfNotification();
				time.id = key;
				time.description = timesOfNotification.get(key);
				time.urgency = this.correctUrgency(urgency);
				timesList.add(time);
			}
		}
		MessageCategory msg = new MessageCategory();
		msg.id = generateMessageId(sourceId, messageCategoryId);
		msg.categoryName = category;
		msg.urgencyList = timesList;
		this.getSettings().addMessageCategory(msg);
		this.saveSettings();
	}

	/**
	 * When a user submits an urgency for a message-category and a specific time
	 * for a message of this category, it will be stored. Any existing urgency
	 * for the given message-category and time will be overwritten.
	 * 
	 * @param userId
	 *            the ID of the user to save the urgency for
	 * @param sourceId
	 *            the ID of the message-category-source
	 * @param messageCategoryId
	 *            the ID of the message-category
	 * @param timeId
	 *            the ID of the time of notification, for which the urgency was
	 *            submitted
	 * @param urgency
	 *            the submitted urgency, ranging from 1 to 5. Any higher
	 *            urgencies than that range will be modified to 5, any lower
	 *            ones to 1
	 */
	public void saveUserUrgency(String userId, String sourceId,
			String messageCategoryId, String timeId, int urgency) {
		String msgId = generateMessageId(sourceId, messageCategoryId);
		urgency = correctUrgency(urgency);
		this.getSettings().addUserUrgency(userId, msgId, timeId, urgency);
		this.saveSettings();
	}

	/**
	 * Calculates a {@link List} of {@link InteractionResource}s, on which the
	 * message of the given message-category will be presented, and the
	 * presentation-mode. The ID of the time of notification, at which the
	 * current calculation takes place, is needed for retrieving the registered
	 * default- and user-urgencies. <br>
	 * <br>
	 * When the category of the message wasn't registered before, the
	 * messageCategoryId and timeOfNotificationId can be omitted. In this case,
	 * the urgency have to be passed directly. When both IDs are passed to this
	 * method, the urgency-parameter is ignored. <br>
	 * <br>
	 * The {@link ContextConnection} is needed for the decisions of usable
	 * devices.
	 * 
	 * @param sourceId
	 *            the source of the message
	 * @param messageCategoryId
	 *            the category-ID of the message
	 * @param userId
	 *            the ID of the user to which the message will be presented
	 * @param timeOfNotificationId
	 *            the ID of the time of notification, at which the current
	 *            calculation takes place
	 * @param urgency
	 *            the urgency for this message, if it is not registered
	 * @param ctxCon
	 *            a {@link ContextConnection} for the decision of usable devices
	 * @return the {@link DecisionResult}, which contains the presentation-mode
	 *         and a {@link List} of the calculated {@link InteractionResource}s
	 */
	public DecisionResult calculatePresentation(String sourceId,
			String messageCategoryId, String userId,
			String timeOfNotificationId, int urgency, ContextConnection ctxCon) {
		String msgId = this.generateMessageId(sourceId, messageCategoryId);
		urgency = correctUrgency(urgency);
		DecisionResult decision = this.decEngine.decidePresentation(msgId,
				userId, timeOfNotificationId, urgency, ctxCon);
		return decision;
	}

	/**
	 * Generates the ID for saving a message-category in or retrieving it from
	 * the {@link NotificationSettings}. The returned ID is an ID only used in
	 * this notification-system for telling message-categories apart without
	 * forcing the message-sources to assign a system-wide unique ID by
	 * themself.
	 * 
	 * @param sourceId
	 *            ID of the message-source
	 * @param messageCategoryId
	 *            ID of the message-category
	 * @return an unique ID for the message-category
	 */
	private String generateMessageId(String sourceId, String messageCategoryId) {
		String msgId = sourceId;
		if (messageCategoryId != null && !messageCategoryId.isEmpty()) {
			msgId = msgId + "." + messageCategoryId;
		}
		return msgId;
	}

	/**
	 * Corrects urgencies, if they are out of the allowed range. Urgencies
	 * higher than 5 will be reduced to 5, those ones lower than 1 will be set
	 * to 1.
	 * 
	 * @param urgency
	 *            the urgency to correct
	 * @return the corrected urgency
	 */
	private int correctUrgency(int urgency) {
		if (urgency > 5) {
			urgency = 5;
		} else if (urgency < 1) {
			urgency = 1;
		}
		return urgency;
	}
}
